iT邦幫忙

2024 iThome 鐵人賽

DAY 4
1

回來做元件!

今天接著介紹如何利用模板動態生成DOM,並比較 React 和 Vue 的不同實作方式。

Functional component vs Composition API

架構設計

將divRef作為參數傳遞給 NavigationBar,根據子元件數量動態生成導航和 href:

function Playground({margin}){
    /* ......  */
    return (
        <>
            <div id="playground" ref={divRef} style={getStyle()}>
                <CanvasSection1 ratio={ratio} min={min}/>
                <CanvasSection2 ratio={ratio} min={min}/>
                <CanvasSection3 ratio={ratio} min={min}/>
            </div>
            <NavigationBar divRef={divRef} width={(ratio == 1) ? 250 : 150}/>
        </>
    )
}

延續先前的設計,使用 ratio 來判斷是否為桌面端或移動端,調整導航欄的寬度。

導航欄的實作

React

接著利用 useState 儲存 isOpen,並在狀態變更時重新渲染導航欄,以 GetHyperLink 導入模板:

function NavigationBar({width, divRef}){
    const [isOpen, setIsOpen] = useState(false);
    return (
        <nav id="nav" style={{
            "left": (isOpen ? 0 : -width) + "px",
            "width": width
        }}>
            <GetHyperLink divRef={divRef}></GetHyperLink>
            <div onClick={() => setIsOpen(!isOpen)} id="navSlider">
                <p>{isOpen ? "X": "≡"}</p>
            </div>
        </nav>
    );
}

這邊不能直接寫 onClick={setIsOpen(!isOpen)},對新手來說,必須注意傳遞給事件的是一個回呼函式,event會作為這個函式的唯一參數,需要用一個函式來包裹它

Vue

<script setup>
    //......
    const props = defineProps({
        width: Number,
        divRef: Object
    });
    const isOpen = ref(false);
    const handleClick = () => isOpen.value = !isOpen.value;
</script>

<template>
    <nav id="nav" :style="{left: isOpen ? '0' : -width + 'px'}">
        <!-- ...... -->
        <div @click="handleClick" id="navSlider">
            <p>{{isOpen ? "X": "≡"}}</p>
        </div>
    </nav>
</template>

動態生成超連結

一個基本的超連結模板如下所示:

<a key="section1" href="#section1" >section1</a>

<a key={ID} href={"#"+ID} >{ID}</a>

key 很重要,是框架用來識別唯一元素的依據,在平級之間不重複即可

那麼,透過 divRef.current 取得 playground 元件中的所有 section ,並根據它們的 ID 生成對應的超連結:

React

直接將 GetHyperLink 作為一個元件,回傳模板:

function GetHyperLink({divRef}){
    const [hyperlink, setHyperlink] = useState();
    useEffect(() => {
        const sections = divRef.current.getElementsByTagName("section");
        setHyperlink(Object.keys(sections).map((key) => {
            const ID = sections[key].id;
            return <a key={ID} className="list" href={"#"+ID} >{ID}</a>
        }));
    }, []);
    return hyperlink;
}

為了使用 map 歷遍每個 section,第一種方法是用 Object.keys 來取得 keys 組成陣列。

Vue

不同於上面的做法,我們將生成的連結儲存為 links 陣列,並利用 v-for 迭代模板。並且,利用 nextTick 方法,等待全部的組件都渲染完,才能正確取得 section 元素:

const links = ref([]);
onMounted(async() => {
    await nextTick();
    const sections = props.divRef.getElementsByTagName('section');
    links.value = Array.from(sections).map(section => ({
        id: section.id,
    }));
});

為了使用 map 歷遍每個 section,第二種方法是利用 sections 可以迭代的特性,使用 from 轉換成陣列。

<template>
    <nav id="nav" :style="{left: isOpen ? '0' : -width + 'px'}">
        <div v-for="link in links" :key="link.id">
            <a :href="'#' + link.id" :id="'to' + link.id" class="list">{{ link.id }}</a>
        </div>
        <!-- ...... -->
    </nav>
</template>

好像比 React 多了一個步驟,但如果資料已經有了,比如從後端取得,就可以直接用 v-for 載入模板,會比 React 簡潔很多,過幾天我們會實作 Table 元件,到時候就有感了!

潛在的問題

由於超連結是在元件渲染後生成,客戶端無法在進到網頁時,利用網址後的hash,立刻跳轉到指定區塊,所以我們還需要利用副作用跳轉:

function handleHashChange(){
    const hash = window.location.hash;
    if (!hash) return;

    const targetElement = document.querySelector(hash);
    if (!targetElement) return;

    targetElement.scrollIntoView({ behavior: 'smooth' });
};

function NavigationBar({width, divRef}){
    useEffect(() => {
        handleHashChange();
        window.addEventListener('hashchange', handleHashChange);
        return () => {
            window.removeEventListener('hashchange', handleHashChange);
        }
    }, []);
    //......
}

渲染完元件後,會立刻執行一次跳轉到指定的區塊,同時監聽 hashchange ,藉此可以客製化更多細節,比如實現水平方向或垂直方向的對齊,詳細可以參考MDN scrollIntoView

元件卸載

最後,別忘了 removeEventListener,剛剛是透過 return 的方式提供卸載所需的函式。在 Vue 中,這樣的監聽與卸載則需要透過 onMounted 和 onUnmounted 實現:

onMounted(() => {
    //......
    handleHashChange();
    window.addEventListener('hashchange', handleHashChange);
});

onUnmounted(() => {
    window.removeEventListener('hashchange', handleHashChange);
});

其實跟 React 的類組件概念差不多!

結語

透過這種動態生成的方式,無論未來添加多少個 section,我們都能自動生成對應的超連結。如果感興趣,可以參考 Github 上的原始碼:
NavigationBar.jsx
NavigationBar.vue

動態生成的概念相當重要,這種方式廣泛應用於文章目錄、影片目錄等場景。語法本身並不困難,主要挑戰在於理解渲染的順序。希望這篇文章能為新手提供一些啟發!


上一篇
A2 腳步踩穩囉:啟動工廠模式下的 Canvas Transition 動畫
下一篇
A4 面板元件-靈活的收納按鈕設計
系列文
讓演算法起舞:前端特效應用的探索之旅35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言